package com.client;

import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson.parser.Feature;
import com.crypto.ECDSASign;
import com.google.common.base.Charsets;
import com.google.protobuf.ByteString;
import com.google.protobuf.InvalidProtocolBufferException;
import com.google.protobuf.Timestamp;
import com.google.protobuf.util.JsonFormat;
import com.protos.Peer.*;
import com.service.callback.CallbackOfTransactionOnChain;
import com.utils.CertUtil;
import com.websocket.wsCallBack;
import com.websocket.wsCbClient;
import org.apache.commons.codec.binary.Base64;
import org.apache.http.Header;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.ContentType;
import org.apache.http.entity.FileEntity;
import org.apache.http.entity.InputStreamEntity;
import org.apache.http.entity.StringEntity;
import org.apache.http.entity.mime.HttpMultipartMode;
import org.apache.http.entity.mime.MultipartEntityBuilder;
import org.apache.http.entity.mime.content.FileBody;
import org.apache.http.entity.mime.content.StringBody;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.util.EntityUtils;

import java.io.*;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.charset.Charset;
import java.security.PrivateKey;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.*;


/**
 * RepChain客户端，接口封装，用于提交交易，获取链数据
 *
 * @author zyf
 */
public class RepChainClient extends Client {

    /**
     * host 为主机地址，jksPath为jks地址路径，password为jks与私钥密码，二者一致，alias为别名，相关四个参数都需要赋值，交易才能签名上链。
     * pemPath为pem地址路径，password为私钥密码，相关3个参数全部赋值，交易才能签名上链。
     */
    protected String host = "localhost:8081";
    protected Boolean keyInit = false;
    protected Boolean jksInit = false;
    protected Boolean pemInit = false;

    protected PrivateKey privateKey;
    protected String signAlgorithm = "SHA1withECDSA";

    private CloseableHttpClient client = HttpClientBuilder.create().build();

    private ExecutorService executor = Executors.newCachedThreadPool();
//    private ExecutorService executor = Executors.newFixedThreadPool(10);


    public RepChainClient() {
    }

    public static RepChainClient getInstance(String type) {
        if (type.equalsIgnoreCase("JKS")) {
            return new RepChainJksClient();
        } else if (type.equalsIgnoreCase("PEM")) {
            return new RepChainPemClient();
        } else if (type.equalsIgnoreCase("KEY")) {
            return new RepChainKeyClient();
        } else {
            return new RepChainClient();
        }
    }

    /**
     * @param host 服务主机地址
     */
    public RepChainClient(String host) {
        this.host = host;
    }

    /**
     * 构造invoke交易,单参数
     *
     * @param certId             证书标识
     * @param chaincodeId        指定合约名称和版本,确定唯一的合约代码
     * @param chaincodeInputFunc 要调用合约中的函数方法
     * @param param              实际方法参数，单参数
     * @return 构造好的交易
     * @throws Exception
     * @see CertId
     * @see ChaincodeId chaincodeId 合约ID
     */
    public Transaction createTransactionForInvoke(CertId certId, ChaincodeId chaincodeId, String chaincodeInputFunc, String param) throws Exception {
        // 系统生成一个id给交易
        String txid = UUID.randomUUID().toString().replace("-", "");
        Transaction tranInv = createTransactionForInvoke(txid, certId, chaincodeId, chaincodeInputFunc, param);
        return tranInv;
    }

    /**
     * 构造invoke交易，多参数
     *
     * @param certId             证书标识
     * @param chaincodeId        指定合约名称和版本,确定唯一的合约代码
     * @param chaincodeInputFunc 要调用合约中的函数方法
     * @param params             实际方法参数，参数集
     * @return 构造好的交易
     * @throws Exception
     * @see CertId
     * @see ChaincodeId chaincodeId 合约ID
     */
    public Transaction createTransactionForInvoke(CertId certId, ChaincodeId chaincodeId, String chaincodeInputFunc, List<String> params) throws Exception {
        // 系统生成一个id给交易
        String txid = UUID.randomUUID().toString().replace("-", "");
        Transaction tranInv = createTran4InvWithTxid(txid, certId, chaincodeId, chaincodeInputFunc, params);
        return tranInv;
    }

    /**
     * 构造invoke交易
     *
     * @param uuid               用户自定义交易id（注意，要唯一）
     * @param certId             证书标识
     * @param chaincodeId        指定合约名称和版本,确定唯一的合约代码
     * @param chaincodeInputFunc 要调用合约中的函数方法
     * @param param              实际方法参数
     * @return 构造好的交易
     * @throws Exception
     * @see CertId
     * @see ChaincodeId chaincodeId 合约ID
     */
    public Transaction createTransactionForInvoke(String uuid, CertId certId, ChaincodeId chaincodeId, String chaincodeInputFunc, String param) throws Exception {
        List params = new ArrayList();
        params.add(param);
        return createTran4InvWithTxid(uuid, certId, chaincodeId, chaincodeInputFunc, params);
    }

    /**
     * 构造invoke交易
     *
     * @param uuid               用户自定义交易id（注意，要唯一）
     * @param certId             证书标识
     * @param chaincodeId        指定合约名称和版本,确定唯一的合约代码
     * @param chaincodeInputFunc 要调用合约中的函数方法
     * @param params             实际方法参数
     * @return 构造好的交易
     * @throws Exception
     * @see CertId
     * @see ChaincodeId chaincodeId 合约ID
     */
    public Transaction createTransactionForInvoke(String uuid, CertId certId, ChaincodeId chaincodeId, String chaincodeInputFunc, List<String> params) throws Exception {
        return createTran4InvWithTxid(uuid, certId, chaincodeId, chaincodeInputFunc, params);
    }

    /**
     * 构造deploy交易
     *
     * @param certId      证书标识
     * @param chaincodeId chaincodeId 合约ID
     * @param spcPackage  合约代码（合约文件的字符串）
     * @param legal_prose 法律文本描述
     * @param timeout     超时
     * @param ctype       合约类型（JavaScript or scala）
     * @return 构建好的交易
     * @throws Exception
     */
    public Transaction createTransactionForDeploy(CertId certId, ChaincodeId chaincodeId, String spcPackage, String legal_prose, int timeout, ChaincodeDeploy.CodeType ctype) throws Exception {
        String txid = UUID.randomUUID().toString().replace("-", "");
        ChaincodeDeploy chaincodeDeploy = ChaincodeDeploy.newBuilder()
                .setTimeout(timeout)
                .setCodePackage(spcPackage)
                .setLegalProse(legal_prose)
                .setCtype(ctype)
                .build();
        Transaction tranDep = Transaction.newBuilder()
                .setId(txid)
                .setType(Transaction.Type.CHAINCODE_DEPLOY)
                .setCid(chaincodeId)
                .setSpec(chaincodeDeploy)
                .build();
        // 默认采用"SHA1withECDSA"作为签名算法
        return signTransaction(tranDep, privateKey, certId);
    }

    /**
     * 禁用或者启用合约
     *
     * @param certId      证书标识
     * @param chaincodeId chaincodeId 合约ID
     * @param state       合约状态，启用或者禁用合约
     * @return 构建好的交易
     */
    public Transaction createTransactionForState(CertId certId, ChaincodeId chaincodeId, Boolean state) throws Exception {
        String txid = UUID.randomUUID().toString().replace("-", "");
        Transaction tranSt = Transaction.newBuilder()
                .setId(txid)
                .setType(Transaction.Type.CHAINCODE_SET_STATE)
                .setCid(chaincodeId)
                .setState(state)
                .build();
        // 默认采用"SHA1withECDSA"作为签名算法
        return signTransaction(tranSt, privateKey, certId);
    }


    /**
     * 构造invoke交易,不带txid
     *
     * @param txid               交易iD
     * @param certId             证书标识
     * @param chaincodeId        指定合约名称和版本,确定唯一的合约代码
     * @param chaincodeInputFunc 要调用合约中的函数方法
     * @param params             实际方法参数
     * @return 构造好的交易
     * @throws Exception
     * @see CertId
     * @see ChaincodeId chaincodeId 合约ID
     */
    private Transaction createTran4InvWithTxid(String txid, CertId certId, ChaincodeId chaincodeId, String chaincodeInputFunc, List<String> params) throws Exception {
        if (txid == "" || txid.trim() == "" || txid == null) {
            txid = UUID.randomUUID().toString().replace("-", "");
        }
        ChaincodeInput ipt = ChaincodeInput.newBuilder()
                .setFunction(chaincodeInputFunc)
                .addAllArgs(params).build();
        Transaction tranInv = Transaction.newBuilder()
                .setId(txid)
                .setType(Transaction.Type.CHAINCODE_INVOKE)
                .setCid(chaincodeId)
                .setIpt(ipt)
                .build();
        // 默认采用"SHA1withECDSA"作为签名算法
        return signTransaction(tranInv, privateKey, certId);
    }

    /**
     * 为交易签名
     *
     * @param transaction 不带签名的交易
     * @param privateKey  私钥
     * @param certId      certId，带着证书标识
     * @return
     */
    protected Transaction signTransaction(Transaction transaction, PrivateKey privateKey, CertId certId) throws Exception {
        checkInitial();
        byte[] sig = new ECDSASign().sign(privateKey, transaction.toByteArray(), signAlgorithm);
        long millis = System.currentTimeMillis() + 8 * 3600 * 1000;
        Signature signature = Signature.newBuilder()
                .setCertId(certId)
                .setTmLocal(Timestamp.newBuilder().setSeconds(millis / 1000).setNanos((int) (millis % 1000) * 1000000))
                .setSignature(ByteString.copyFrom(sig))
                .build();
        Transaction.Builder builder = transaction.toBuilder();
        builder.setSignature(signature);
        return builder.build();
    }

    /**
     * 获取区块链信息
     *
     * @return 当前区块链高度、总共交易数、currentBlockHash、previousBlockHash等信息
     */
    public JSONObject getChainInfo() {
        JSONObject jsonObject = doGet("http://" + host + "/chaininfo");
        return jsonObject;
    }


    public JSONObject getChainInfoNode() {
        JSONObject jsonObject = doGet("http://" + host + "/chaininfo/node");
        return jsonObject;
    }

    /**
     * 获取区块高度
     *
     * @return 当前链区块高度
     */
    public int getChainHeight() {
        JSONObject chainInfo = getChainInfo();
        int height = chainInfo.getJSONObject("result").getIntValue("height");
        return height;
    }

    public int getTotalTransNum() {
        JSONObject chainInfo = getChainInfo();
        int totalTransQuantity = chainInfo.getJSONObject("result").getIntValue("totalTransactions");
        return totalTransQuantity;
    }


    /**
     * 获取当前块的hash值，Base64字符串形式
     *
     * @return Base64字符串形式的hash值
     */
    public String getCurrentBlockHash() {
        JSONObject chainInfo = getChainInfo();
        String currentBlockHash = chainInfo.getJSONObject("result").getString("currentBlockHash");
        return currentBlockHash;
    }

    /**
     * 根据高度获取块
     *
     * @param height 区块高度
     * @return Block
     */
    public Block getBlockByHeight(int height) {
        // 根据高度获取块数据
        JSONObject jsonObject = doGet("http://" + host + "/block/" + height);
        JSONObject result = jsonObject.getJSONObject("result");
        String json = result.toJSONString();
        Block.Builder builder = Block.newBuilder();
        try {
            JsonFormat.parser().merge(json, builder);
        } catch (InvalidProtocolBufferException e) {
            e.printStackTrace();
        }
        Block block = builder.build();
        return block;
    }

    /**
     * 根据高度获取块（流式获取）
     *
     * @param height 区块高度
     * @return 返回对应高度的块（流式获取）
     */
    public Block getBlockStreamByHeight(int height) {
        super.setUrl("http://" + host + "/block/stream/" + height);
        HttpResponse response = doGet();
        InputStream inputStream = null;
        Block block = null;
        try {
            inputStream = response.getEntity().getContent();
            block = Block.parseFrom(inputStream);
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (inputStream != null) {
                try {
                    inputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
        return block;
    }

    /**
     * 根据blockHash获取block
     *
     * @param blockHash Block的hash值,Base64字符串
     * @return Block 返回检索到的块
     */
    public Block getBlockByBlockHash(String blockHash) {
        JSONObject jsonObject = doGet("http://" + host + "/block/hash/" + blockHash);
        JSONObject result = jsonObject.getJSONObject("result");
        if (result.isEmpty()) {
            return null;
        }
        String json = result.toJSONString();
        Block block = getBlockByJson(json);
        return block;
    }

    /**
     * 根据json构造Block
     *
     * @param json
     * @return
     */
    private Block getBlockByJson(String json) {
        Block.Builder builder = Block.newBuilder();
        try {
            JsonFormat.parser().merge(json, builder);
        } catch (InvalidProtocolBufferException e) {
            e.printStackTrace();
        }
        return builder.build();
    }

    /**
     * 根据blockHash获取block
     *
     * @param blockHash Block的hash值
     * @return Block 返回检索到的块
     */
    public Block getBlockByBlockHash(ByteString blockHash) {
        String hash = Base64.encodeBase64String(blockHash.toByteArray());
        Block block = getBlockByBlockHash(hash);
        return block;
    }

    /**
     * 根据txid查询交易
     *
     * @param txid 交易txid
     * @return 返回检索到的交易
     */
    public Transaction getTransactionByTxid(String txid) {
        JSONObject jsonObject = doGet("http://" + host + "/transaction/" + txid);
        JSONObject result = jsonObject.getJSONObject("result");
        if (result == null || result.isEmpty()) {
            return null;
        }
        Transaction.Builder builder = Transaction.newBuilder();
        try {
            String json = result.toJSONString();
            JsonFormat.parser().merge(json, builder);
        } catch (InvalidProtocolBufferException e) {
            e.printStackTrace();
        }
        return builder.build();
    }

    /**
     * 根据txid查询交易信息和所在区块高度
     *
     * @param txid 交易txid
     * @return 返回检索到的交易
     */
    public TranInfoAndHeight getTranInfoAndHeightByTxid(String txid) {
        JSONObject jsonObject = doGet("http://" + host + "/transaction/tranInfoAndHeight/" + txid);
        JSONObject result = jsonObject.getJSONObject("result");
        if (result == null || result.isEmpty()) {
            return null;
        }
        Transaction.Builder tranBuilder = Transaction.newBuilder();
        try {
            String json = result.getString("tranInfo");
            JsonFormat.parser().merge(json, tranBuilder);

        } catch (InvalidProtocolBufferException e) {
            e.printStackTrace();
        }
        Long height = result.getLong("height");
        TranInfoAndHeight tranInfoAndHeight = new TranInfoAndHeight(tranBuilder.build(), height);
        return tranInfoAndHeight;
    }

    /**
     * 通过txid查询交易（流式获取）
     *
     * @param txid 交易ID
     * @return 返回检索到的交易（流式获取）
     */
    public Transaction getTransactionStreamByTxid(String txid) {
        super.setUrl("http://" + host + "/transaction/stream/" + txid);
        HttpResponse response = doGet();
        InputStream inputStream = null;
        Transaction transaction = null;
        try {
            inputStream = response.getEntity().getContent();
            transaction = Transaction.parseFrom(inputStream);
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (inputStream != null) {
                try {
                    inputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
        return transaction;
    }

    /**
     * 获取交易的function
     *
     * @param tran 交易
     * @return 返回交易的function
     */
    public String getFunction(Transaction tran) {
        String function = tran.getIpt().getFunction();
        return function;
    }

    /**
     * post交易到RepChain
     *
     * @param json 提交的json数据
     * @return
     */
    public JSONObject postTranByString(String json) throws RuntimeException {
        String url = "http://" + host + "/transaction/postTranByString";
        JSONObject jsonObject = doPost(url, json);
        return jsonObject;
    }

    /**
     * 提交交易并回调提交交易的结果结果给callWs
     *
     * @param callWs      回调函数
     * @param json        提交的json数据
     * @param transaction 需要提交的交易
     */
    /*public void postTranByString(final wsCallBack callWs, final String json, final Transaction transaction) throws RuntimeException {
//        final wsCbClient[] server = {null};
        executor.submit(new Runnable() {
            wsCbClient server;
            Boolean connect;

            @Override
            public void run() {
                try {
                    server = new wsCbClient(new URI("ws://" + host + "/event"));
                    // 阻塞，等待连接好
                    connect = server.connectBlocking();
                } catch (Exception e) {
                    e.printStackTrace();
                } finally {
                    System.out.println(server.isOpen());
                    JSONObject result = postTranByString(json);
                    if (result.getString("err") == null) {
                        callWs.preDoTransaction(true, result);
                        if (connect) {
                            server.feedBack(callWs, transaction);
                        } else {
                            callWs.verifyBlock(false, null, "无法确认是否出块");
                        }
                    } else {
                        callWs.preDoTransaction(false, result);
                        callWs.verifyBlock(false, null, "预执行失败，无法出块");
                        server.getConnection().close();
                    }
                }
            }
        });
    }*/

    public void postTranByString(final wsCallBack callWs, final String json, final Transaction transaction) throws RuntimeException {
        String txid = transaction.getId();
        CallbackOfTransactionOnChain ctc = CallbackOfTransactionOnChain.getPush(host);
        ctc.addCallbackInfo(txid,callWs);
        JSONObject result = postTranByString(json);
        if (result.getString("err") == null) {
            callWs.preDoTransaction(true, result);
        }else{
            callWs.preDoTransaction(false, result);
            callWs.onChain(txid,false);
            ctc.removeCallbackInfo(txid);
        }
    }

    /**
     * 流式提交交易
     *
     * @param tran Transaction
     * @return 预执行结果
     * @throws RuntimeException
     */
    public JSONObject postTranByStream(Transaction tran) throws RuntimeException {
        String url = "http://" + host + "/transaction/postTranStream";
        JSONObject jsonObject = doPost(url, tran);
        return jsonObject;
    }

    /**
     * post交易到RepChain
     *
     * @param url  请求路径
     * @param json 要提交的json数据
     * @return 返回post结果
     */
    public JSONObject doPost(String url, String json) {
//        CloseableHttpClient client = HttpClientBuilder.create().build();
        HttpPost post = new HttpPost(url);
        JSONObject jsonObject = new JSONObject(true);
        HttpEntity resEntity = null;
        try {
            StringEntity s = new StringEntity(json);
            s.setContentEncoding("UTF-8");
            //发送json数据需要设置contentType
            s.setContentType("application/json");
            post.setEntity(s);
            HttpResponse response = client.execute(post);
            // 响应状态码
            int statusCode = response.getStatusLine().getStatusCode();
            resEntity = response.getEntity();
            String str = EntityUtils.toString(resEntity,"UTF-8");
            jsonObject = (JSONObject) JSONObject.parse(str, Feature.OrderedField);
        } catch (Exception ex) {
            throw new RuntimeException(ex);
        } finally {
            if (resEntity != null) {
                try {
                    EntityUtils.consume(resEntity);
                } catch (Exception ex) {
                    ex.printStackTrace();
                }
            }
        }
        return jsonObject;
    }

    /**
     * 提交交易
     *
     * @param url  请求路径
     * @param tran 交易字节流
     * @return 返回post的结果（该方法用于流式提交交易）
     */
    protected JSONObject doPost(String url, Transaction tran) {
//        checkInitial();
//        CloseableHttpClient client = HttpClientBuilder.create().build();
        HttpPost post = new HttpPost(url);
        JSONObject jsonObject = new JSONObject(true);
        HttpEntity resEntity = null;
        try {
            File tempFile = File.createTempFile("tran", null);
            OutputStream os = new FileOutputStream(tempFile);
            os.write(tran.toByteArray());
            os.close();
            MultipartEntityBuilder builder = MultipartEntityBuilder.create();
            HttpEntity httpEntity = builder.setMode(HttpMultipartMode.BROWSER_COMPATIBLE)
                    .addPart("signedTrans", new FileBody(tempFile)).build();
//            FileEntity fileEntity = new FileEntity(tempFile);
//            String BOUNDARY = "----------" + System.currentTimeMillis();
//            post.setHeader("Content-Type", "multipart/form-data; boundary=" + BOUNDARY );//发送数据需要设置contentType
//            post.setHeader("Content-Type", ContentType.MULTIPART_FORM_DATA.toString());
//            fileEntity.setContentEncoding("UTF-8");
            post.setEntity(httpEntity);
            HttpResponse response = client.execute(post);
            tempFile.delete();
            // 响应状态码
            int statusCode = response.getStatusLine().getStatusCode();
            resEntity = response.getEntity();
            String str = EntityUtils.toString(resEntity,"UTF-8");
            jsonObject = (JSONObject) JSONObject.parse(str, Feature.OrderedField);
        } catch (Exception ex) {
            throw new RuntimeException(ex);
        } finally {
            if (resEntity != null) {
                try {
                    EntityUtils.consume(resEntity);
                } catch (Exception ex) {
                    ex.printStackTrace();
                }
            }
        }
        return jsonObject;
    }

    /**
     * get请求区块链数据
     *
     * @param url 请求的路径
     * @return JSONObject 返回结果
     */
    public JSONObject doGet(String url) {
//        CloseableHttpClient client = HttpClientBuilder.create().build();
        HttpGet get = new HttpGet(url);
        JSONObject jsonObject = new JSONObject(true);
        HttpEntity resEntity = null;
        try {
            HttpResponse response = client.execute(get);
            resEntity = response.getEntity();
            String str = EntityUtils.toString(response.getEntity(),"UTF-8");
            jsonObject = (JSONObject) JSONObject.parse(str, Feature.OrderedField);
        } catch (Exception ex) {
            ex.printStackTrace();
        } finally {
            if (resEntity != null) {
                try {
                    EntityUtils.consume(resEntity);
                } catch (Exception ex) {
                    ex.printStackTrace();
                }
            }
        }
        return jsonObject;
    }

    /**
     * 检查是否初始化成功
     */
    private void checkInitial() {
        if (jksInit && pemInit && keyInit) {
            throw new RuntimeException("未设置私钥:Jks、Pem 和 Key均未设置");
        }
    }

    public void setHost(String host) {
        this.host = host;
    }

    public String getHost() {
        return host;
    }

    public String getSignAlgorithm() {
        return signAlgorithm;
    }

    public void setSignAlgorithm(String signAlgorithm) {
        this.signAlgorithm = signAlgorithm;
    }

}

/**
 * TranInfoAndHeight
 */
class TranInfoAndHeight {

    private Transaction tranInfo;
    private Long height;

    public TranInfoAndHeight(Transaction tranInfo, Long height) {
        this.tranInfo = tranInfo;
        this.height = height;
    }

    public Transaction getTranInfo() {
        return tranInfo;
    }

    public Long getHeight() {
        return height;
    }
}
